#!/usr/bin/perl
use strict;
use FindBin;

package K8sDeploy;

use JSON;
use YAML::Tiny;
use REST::Client;
use Getopt::Long;
use JSON qw(to_json from_json);
use AnyEvent::HTTP;
use URI::Escape qw(uri_escape);
use POSIX qw(strftime);

use WebCtl;
use AutoExecUtils;
use DeployUtils;

sub usage {
    my $pname = $FindBin::Script;

    print("Usage: $pname --host K8sHost --apiport APIServerPort --mgmtport ManagermentPort --token Token\n");
    print("       --namespace K8sNameSpace --deployment K8sDeployment\n");
    print("\n");
    print("       --host:         k8s host, example:192.168.99.100\n");
    print("       --apiport:      k8s api port, example:8843\n");
    print("       --token:        k8s authenticate token\n");
    print("       --dplyyamlfile: k8s deployment yaml config file path(relative to artifact root dir)\n");
    print("       --namespace:    k8s namespace, example:default\n");
    print("       --deployment:   k8s deployment: example:nginx-deployment\n");
    print("       --acton:        k8s deploy action deploy|stop|start|rollback\n");
    print("       --autocreate:   auto create none exist deployment\n");
    print("       --imagelist:    Image list, example:repository/name:tag\n");

    exit(1);
}

sub new {
    my ( $type, %args ) = @_;
    my $self = \%args;
    $self->{optionError} = 0;
    $self->{hasError}    = 0;
    $self->{tailLogPods} = {};
    $self->{updateTime}  = time();

    if ( not defined( $self->{replicas} ) ) {
        $self->{replicas} = 0;
    }

    my $webCtl = WebCtl->new();

    #使用Header的基于token的认证
    $webCtl->setHeaders(
        {
            'Authorization' => 'Bearer ' . $args{token},
            'Content-Type'  => 'application/json'
        }
    );
    $self->{webCtl} = $webCtl;

    bless( $self, $type );
    return $self;
}

# sub DESTROY {
#     my ($self) = @_;
#     my $tailLogPodsMap = $self->{tailLogPods};
#     foreach my $podName ( keys(%$tailLogPodsMap) ) {
#         my $guard = delete( $tailLogPodsMap->{$podName} );
#         undef($guard);
#     }
# }
sub getApiUrl {
    my ( $self, $kind, $apiVersion, $namespace, $objName, $queryMap ) = @_;

    my $baseUrl = $self->{baseUrl};
    if ( not defined($namespace) or $namespace eq '' ) {
        $namespace = $self->{namespace};
    }

    my $apiGroup;
    if ( $apiVersion =~ /^(\w+)\/v\d+$/ ) {
        $apiGroup = $1;
    }

    my $url;
    if ( defined($apiGroup) ) {
        $url = "$baseUrl/apis/$apiVersion/namespaces/$namespace/" . lc($kind) . 's';
    }
    else {
        $url = "$baseUrl/api/$apiVersion/namespaces/$namespace/" . lc($kind) . 's';
    }

    if ( defined($objName) and $objName ne '' ) {
        $url = $url . '/' . $objName;
    }

    if ( defined( $self->{dryRun} ) ) {
        if ( defined($queryMap) ) {
            $queryMap->{dryRun} = $self->{dryRun};
        }
        else {
            $queryMap = { dryRun => $self->{dryRun} };
        }
    }

    if ( defined($queryMap) ) {
        my $queryStr = '?';
        while ( my ( $key, $value ) = each(%$queryMap) ) {
            $queryStr = $queryStr . uri_escape($key) . '=' . uri_escape($value) . '&';
        }
        $queryStr = substr( $queryStr, 0, -1 );
        $url      = $url . $queryStr;
    }

    return $url;
}

sub getResource {
    my ( $self, $kind, $apiVersion, $namespace, $objName, $queryMap ) = @_;

    my $webCtl = $self->{webCtl};
    my $url    = $self->getApiUrl( $kind, $apiVersion, $namespace, $objName, $queryMap );

    my $client = $webCtl->{restClient};

    my $rc;
    my $content;
    eval {
        $content = $webCtl->doRest( 'GET', $url );
        $rc      = from_json($content);
    };
    if ($@) {
        if ( $client->responseCode() eq 404 ) {
            $content = $webCtl->convCharSet( $client->responseContent() );
            $rc      = from_json($content);
        }
        else {
            my $msg = $@;
            $msg =~ s/ at .*?$//;
            die($msg);
        }
    }

    return $rc;
}

sub patchResource {
    my ( $self, $resourcePatch, $kind, $apiVersion, $namespace, $objName, $queryMap ) = @_;

    my $webCtl = $self->{webCtl};
    my $url    = $self->getApiUrl( $kind, $apiVersion, $namespace, $objName, $queryMap );

    my $header = { Content_Type => 'application/strategic-merge-patch+json' };
    $self->{updateTime} = time();
    my $content = $webCtl->doRest( 'PATCH', $url, $resourcePatch, $header );
    my $rc      = from_json($content);
    if ( $rc->{kind} eq 'Status' ) {
        $self->{hasError} = 1;
        print( "ERROR: Patch resource $kind:$namespace/$objName failed, " . $rc->{message} . "\n" );
        return;
    }
    return $rc;
}

sub deleteResource {
    my ( $self, $kind, $apiVersion, $namespace, $objName, $queryMap ) = @_;

    my $webCtl = $self->{webCtl};
    my $url    = $self->getApiUrl( $kind, $apiVersion, $namespace, $objName, $queryMap );

    $self->{updateTime} = time();
    my $content = $webCtl->doRest( 'DELETE', $url );
    my $rc      = from_json($content);
    if ( $rc->{kind} eq 'Status' ) {
        $self->{hasError} = 1;
        print( "ERROR: Delete resource $kind:$namespace/$objName failed, " . $rc->{message} . "\n" );
        return;
    }
    return $rc;
}

sub putResource {
    my ( $self, $resource, $kind, $apiVersion, $namespace, $queryMap ) = @_;

    my $webCtl  = $self->{webCtl};
    my $url     = $self->getApiUrl( $kind, $apiVersion, $namespace, undef, $queryMap );
    my $objName = $resource->{metadata}->{name};

    $self->{updateTime} = time();
    my $content = $webCtl->doRest( 'PUT', $url, $resource );
    my $rc      = from_json($content);
    if ( $rc->{kind} eq 'Status' ) {
        $self->{hasError} = 1;
        print( "ERROR: Update resource $kind:$namespace/$objName failed, " . $rc->{message} . "\n" );
        return;
    }
    return $rc;
}

sub createResource {
    my ( $self, $resource, $kind, $apiVersion, $namespace, $queryMap ) = @_;

    my $webCtl  = $self->{webCtl};
    my $url     = $self->getApiUrl( $kind, $apiVersion, $namespace, undef, $queryMap );
    my $objName = $resource->{metadata}->{name};

    $self->{updateTime} = time();
    my $content = $webCtl->doRest( 'POST', $url, $resource );
    my $rc      = from_json($content);
    if ( $rc->{kind} eq 'Status' ) {
        $self->{hasError} = 1;
        print( "ERROR: Create resource $kind:$namespace/$objName failed, " . $rc->{message} . "\n" );
        return;
    }
    return $rc;
}

sub checkConfiHealthCheck {
    my ( $self, $containers, $justCheck ) = @_;

    if ( not defined($justCheck) ) {
        $justCheck = 0;
    }

    my $confOk = 1;
    foreach my $container (@$containers) {
        my $containerName  = $container->{name};
        my $containerImage = $container->{image};
        if ( not defined( $container->{readinessProbe} ) ) {
            if ( $justCheck == 0 and $self->{healthCheckNeeded} == 1 ) {
                $confOk = 0;
                print("ERROR: .spec.template.spec.containers.readinessProbe not defined in container $containerName($containerImage), can not check the pod healthy.\n");
            }
            else {
                print("WARN: .spec.template.spec.containers.readinessProbe not defined in container $containerName($containerImage), can not check the pod healthy.\n");
            }
        }
        if ( not defined( $container->{livenessProbe} ) ) {
            if ( $justCheck == 0 and $self->{healthCheckNeeded} == 1 ) {
                $confOk = 0;
                print("ERROR: .spec.template.spec.containers.livenessProbe not defined in container $containerName($containerImage), can not check the healty while running.\n");
            }
            else {
                print("WARN: .spec.template.spec.containers.livenessProbe not defined in container $containerName($containerImage), can not check the healty while running.\n");
            }
        }
    }

    return $confOk;
}

sub saveDeploymentStatus {
    my ( $self, $deployDetail ) = @_;
    my $data       = {};
    my $deployment = $deployDetail->{deployment};
    $data->{deployment} = {
        name              => $deployment->{metadata}->{name},
        namespace         => $deployment->{metadata}->{namespace},
        uid               => $deployment->{metadata}->{uid},
        labels            => $deployment->{metadata}->{labels},
        revision          => $deployment->{metadata}->{annotations}->{'deployment.kubernetes.io/revision'},
        creationTimestamp => $deployment->{metadata}->{creationTimestamp},
        replicas          => $deployment->{status}->{replicas},
        updatedReplicas   => $deployment->{status}->{updatedReplicas},
        readyReplicas     => $deployment->{status}->{readyReplicas},
        availableReplicas => $deployment->{status}->{availableReplicas},
    };

    foreach my $statusCondition ( @{ $deployment->{status}->{conditions} } ) {
        if ( defined( $statusCondition->{lastTransitionTime} ) ) {
            $data->{deployment}->{lastTransitionTime} = $statusCondition->{lastTransitionTime};
            last;
        }
    }

    my $newReplicaset = $deployDetail->{newreplicaset};
    $data->{newreplicaset} = {
        name              => $newReplicaset->{metadata}->{name},
        namespace         => $newReplicaset->{metadata}->{namespace},
        uid               => $newReplicaset->{metadata}->{uid},
        labels            => $newReplicaset->{metadata}->{labels},
        revision          => $newReplicaset->{metadata}->{annotations}->{'deployment.kubernetes.io/revision'},
        creationTimestamp => $newReplicaset->{metadata}->{creationTimestamp},
        replicas          => $newReplicaset->{status}->{replicas},
        readyReplicas     => $newReplicaset->{status}->{readyReplicas},
        availableReplicas => $newReplicaset->{status}->{availableReplicas}
    };

    if ( not defined( $data->{newreplicaset}->{readyReplicas} ) ) {
        $data->{newreplicaset}->{readyReplicas} = 0;
    }

    my @newPods = ();
    foreach my $pod ( @{ $newReplicaset->{pods} } ) {
        my $podInfo = {
            name              => $pod->{metadata}->{name},
            namespace         => $pod->{metadata}->{namespace},
            uid               => $pod->{metadata}->{uid},
            labels            => $pod->{metadata}->{labels},
            status            => $pod->{status}->{phase},
            nodeName          => $pod->{spec}->{nodeName},
            creationTimestamp => $pod->{metadata}->{creationTimestamp},
            startTime         => $pod->{status}->{startTime},
            podIP             => $pod->{status}->{podIP},
            podIPs            => $pod->{status}->{podIPs}
        };

        my $containerCount = scalar( @{ $pod->{spec}->{containers} } );
        if ( not defined($containerCount) ) {
            $containerCount = 0;
        }

        my @containers = ();
        my $readyCount = 0;
        foreach my $containerStatus ( @{ $pod->{status}->{containerStatuses} } ) {
            if ( $containerStatus->{ready} ) {
                $readyCount = $readyCount + 1;
            }
            push(
                @containers,
                {
                    name         => $containerStatus->{name},
                    image        => $containerStatus->{image},
                    restartCount => $containerStatus->{restartCount},
                    ready        => $containerStatus->{ready},
                }
            );
        }
        $podInfo->{containerCount}      = $containerCount;
        $podInfo->{readyContainerCount} = $readyCount;
        if ( $readyCount == $containerCount ) {
            $podInfo->{ready} = 1;
        }
        $podInfo->{containers} = \@containers;
        push( @newPods, $podInfo );
    }
    $data->{newreplicaset}->{pods} = \@newPods;

    my $oldReplicaset = $deployDetail->{oldreplicaset};
    if ( defined($oldReplicaset) ) {
        $data->{oldreplicaset} = {
            name              => $oldReplicaset->{metadata}->{name},
            namespace         => $oldReplicaset->{metadata}->{namespace},
            uid               => $oldReplicaset->{metadata}->{uid},
            labels            => $oldReplicaset->{metadata}->{labels},
            revision          => $oldReplicaset->{metadata}->{annotations}->{'deployment.kubernetes.io/revision'},
            creationTimestamp => $oldReplicaset->{metadata}->{creationTimestamp},
            replicas          => $oldReplicaset->{status}->{replicas},
            readyReplicas     => $oldReplicaset->{status}->{readyReplicas},
            availableReplicas => $oldReplicaset->{status}->{availableReplicas}
        };

        if ( not defined( $data->{oldreplicaset}->{readyReplicas} ) ) {
            $data->{oldreplicaset}->{readyReplicas} = 0;
        }

        my @oldPods = ();
        foreach my $pod ( @{ $oldReplicaset->{pods} } ) {
            my $podInfo = {
                name              => $pod->{metadata}->{name},
                namespace         => $pod->{metadata}->{namespace},
                uid               => $pod->{metadata}->{uid},
                labels            => $pod->{metadata}->{labels},
                status            => $pod->{status}->{phase},
                nodeName          => $pod->{spec}->{nodeName},
                creationTimestamp => $pod->{metadata}->{creationTimestamp},
                startTime         => $pod->{status}->{startTime},
                podIP             => $pod->{status}->{podIP},
                podIPs            => $pod->{status}->{podIPs}
            };

            my $containerCount = scalar( @{ $pod->{spec}->{containers} } );
            if ( not defined($containerCount) ) {
                $containerCount = 0;
            }

            my @containers = ();
            my $readyCount = 0;
            foreach my $containerStatus ( @{ $pod->{status}->{containerStatuses} } ) {
                if ( $containerStatus->{ready} ) {
                    $readyCount = $readyCount + 1;
                }
                push(
                    @containers,
                    {
                        name         => $containerStatus->{name},
                        image        => $containerStatus->{image},
                        restartCount => $containerStatus->{restartCount},
                        ready        => $containerStatus->{ready},
                    }
                );
            }
            $podInfo->{containerCount}      = $containerCount;
            $podInfo->{readyContainerCount} = $readyCount;
            if ( $readyCount == $containerCount ) {
                $podInfo->{ready} = 1;
            }
            $podInfo->{containers} = \@containers;
            push( @oldPods, $podInfo );
        }
        $data->{oldreplicaset}->{pods} = \@oldPods;
    }
    AutoExecUtils::saveLiveData( { k8s_data => $data } );
}

sub getDeployment {
    my ( $self, $checkConf ) = @_;

    if ( not defined($checkConf) ) {
        $checkConf = 1;
    }

    my $namespace  = $self->{namespace};
    my $deployment = $self->{deployment};
    my $rc         = $self->getResource( 'Deployment', 'apps/v1', $namespace, $deployment );

    if ( $rc->{kind} eq 'Deployment' ) {
        $self->{deploymentLabels} = $rc->{spec}->{selector}->{matchLabels};
        $self->{replicas}         = $rc->{spec}->{replicas};
        $self->{revision}         = $rc->{metadata}->{annotations}->{'deployment.kubernetes.io/revision'};
        my $lastConfJson = $rc->{metadata}->{annotations}->{'kubectl.kubernetes.io/last-applied-configuration'};
        if ( defined($lastConfJson) and $lastConfJson ne '' ) {
            $self->{lastConf} = from_json($lastConfJson);
        }

        if ( $checkConf == 1 ) {
            my $containers = $rc->{spec}->{template}->{spec}->{containers};
            $self->checkConfiHealthCheck( $containers, 1 );
        }
    }
    elsif ( $rc->{kind} eq 'Status' ) {
        print( 'WARN: ' . $rc->{message} . ".\n" );
        return undef;
    }
    return $rc;
}

sub createDeployment {
    my ( $self, $depoymentSpec ) = @_;
    my $namespace  = $self->{namespace};
    my $deployment = $self->{deployment};

    return $self->createResource( $depoymentSpec, 'Deployment', 'apps/v1', $namespace );
}

sub patchDeployment {
    my ( $self, $resourceSpecs ) = @_;
    my $namespace  = $self->{namespace};
    my $deployment = $self->{deployment};

    return $self->patchResource( $resourceSpecs, 'Deployment', 'apps/v1', $namespace, $deployment );
}

sub deployVersion {
    my ( $self, $dplyYamlFile, $imageList, $autoCreate ) = @_;

    my $deployment       = $self->{deployment};
    my $defaultNamespace = $self->{namespace};

    my @dplyYamlFiles = ();
    if ( defined($dplyYamlFile) and $dplyYamlFile ne '' ) {
        foreach my $aDplyYamlFile ( split( /\s*,\s*/, $dplyYamlFile ) ) {
            if ( not -f $aDplyYamlFile ) {
                my $deployUtils = DeployUtils->new();
                my $deployEnv   = $deployUtils->deployInit();
                my $dirInfo     = $deployUtils->getDataDirStruct($deployEnv);
                my $mirrorDir   = $dirInfo->{mirror};
                my $appDistPath = $dirInfo->{appdist};

                my @files = glob("$appDistPath/$aDplyYamlFile");
                if (@files) {
                    push( @dplyYamlFiles, @files );
                }
                else {
                    @files = glob("$mirrorDir/$aDplyYamlFile");
                    if (@files) {
                        push( @dplyYamlFiles, @files );
                    }
                    else {
                        print("ERROR: Can not find yaml deploy file:$aDplyYamlFile in artifact directory $appDistPath or $mirrorDir.\n");
                    }
                }

            }
            else {
                push( @dplyYamlFiles, $aDplyYamlFile );
            }
        }
        if ( not @dplyYamlFiles ) {
            $self->{optionError} = 1;
            print("ERROR: Can not find any file for: $dplyYamlFile.\n");
        }
    }
    else {
        if ( not @$imageList ) {
            print("ERROR: Must defind images list by option --imagelist\n");
            $self->{optionError} = 1;
        }
    }

    if ( $self->{optionError} != 0 ) {
        $self->{hasError} = 1;
        return;
    }

    my $hasConfErr    = 0;
    my @resourceSpecs = ();
    if (@dplyYamlFiles) {
        my @yamlDocs = ();
        foreach my $aYamlFile (@dplyYamlFiles) {
            my $aYamlDocs = YAML::Tiny->read($aYamlFile);
            push( @yamlDocs, @$aYamlDocs );
        }

        foreach my $resourceSpec (@yamlDocs) {
            if ( not defined( $resourceSpec->{kind} ) ) {
                $hasConfErr = 1;
                print(qq{ERROR: Attribute ".kind" not defined in yaml file.\n});
            }

            if ( not defined( $resourceSpec->{apiVersion} ) ) {
                $hasConfErr = 1;
                print(qq{ERROR: Attribute ".apiVersion" not defined in yaml file.\n});
            }

            if ( not defined( $resourceSpec->{metadata} ) ) {
                $hasConfErr = 1;
                print(qq{ERROR: Attribute ".metadata" not defined in yaml file.\n});
            }
            else {
                if ( not defined( $resourceSpec->{metadata}->{name} ) ) {
                    print(qq{ERROR: Attribute ".metadata.name" not defined in yaml file.\n});
                }
                if ( not defined( $resourceSpec->{metadata}->{labels} ) or not( %{ $resourceSpec->{metadata}->{labels} } ) ) {
                    $hasConfErr = 1;
                    print(qq{ERROR: Attribute ".labels" not defined in yaml file.\n});
                }
                if ( $defaultNamespace ne $resourceSpec->{metadata}->{namespace} ) {
                    $resourceSpec->{metadata}->{namespace} = $defaultNamespace;
                    print("INFO: Reset $resourceSpec->{kind} .metadata.namespace to $defaultNamespace.\n");
                }
            }

            if ( not defined( $resourceSpec->{spec} ) ) {
                $hasConfErr = 1;
                print(qq{ERROR: Attribute ".spec" not defined in yaml file.\n});
            }
            elsif ( $resourceSpec->{kind} eq 'Deployment' ) {
                if ( defined( $self->{deployment} ) and $self->{deployment} ne '' ) {
                    if ( $self->{deployment} ne $resourceSpec->{metadata}->{name} ) {
                        $resourceSpec->{metadata}->{name} = $self->{deployment};
                        print("INFO: Reset Deployment .metadata.name to $self->{deployment}.\n");
                    }
                }

                if ( not defined( $resourceSpec->{spec}->{template} ) ) {
                    $hasConfErr = 1;
                    print(qq{ERROR: Attribute ".spec.template" not defined in yaml file.\n});
                }
                else {
                    if ( not defined( $resourceSpec->{spec}->{template}->{metadata} ) ) {
                        $hasConfErr = 1;
                        print(qq{ERROR: Attribute ".spec.template.metadata" not defined in yaml file.\n});
                    }
                    else {
                        my $labels = $resourceSpec->{spec}->{template}->{metadata}->{labels};
                        if ( not defined($labels) or not(%$labels) ) {
                            $hasConfErr = 1;
                            print(qq{ERROR: Attribute ".spec.template.metadata.labels" not defined in yaml file.\n});
                        }
                    }
                    my $containers = $resourceSpec->{spec}->{template}->{spec}->{containers};
                    if ( not $self->checkConfiHealthCheck($containers) ) {
                        $hasConfErr = 1;
                    }
                }
                if ( defined( $resourceSpec->{spec}->{replicas} ) ) {
                    $self->{replicas} = $resourceSpec->{spec}->{replicas};
                }
            }

            if ( $hasConfErr == 1 ) {
                $self->{hasError} = 1;
                next;
            }

            if ( $resourceSpec->{kind} eq 'Deployment' ) {
                my $containers      = $resourceSpec->{spec}->{template}->{spec}->{containers};
                my $containersCount = scalar(@$containers);
                my $imageListCount  = scalar(@$imageList);

                if ( $containersCount == 0 ) {
                    foreach my $imageInfo (@$imageList) {
                        push( @$containers, $imageInfo );
                    }
                }
                elsif (@$imageList) {
                    print("INFO: Image list count defined in option are equal to yaml file specified.\n");
                    print("INFO: Try to reset the image specified in yaml file by name.\n");
                    my $imagesMap = {};
                    foreach my $imageInfo (@$imageList) {
                        $imagesMap->{ ( $imageInfo->{name} ) } = $imageInfo;
                    }

                    my $imagesResetCount = 0;
                    foreach my $container (@$containers) {
                        my $containerName = lc( $container->{name} );
                        my $imageInfo     = $imagesMap->{$containerName};
                        if ( defined($imageInfo) ) {
                            $imagesResetCount = $imagesResetCount + 1;
                            $container->{image} = $imageInfo->{image};
                            print("INFO: Reset container $container->{name} image to $imageInfo->{image}.\n");
                        }
                        else {
                            #$container->{image} = lc( $container->{image} );
                            print("ERROR: Container $containerName not found in imagelist option value.\n");
                        }
                    }
                    if ( $imagesResetCount != $containersCount ) {
                        $hasConfErr = 1;
                        print("ERROR: There are $containersCount containers in yaml spec, but only $imagesResetCount found in imagelist option value.\n");
                    }
                }
                else {
                    foreach my $container (@$containers) {
                        $container->{image} = lc( $container->{image} );
                    }
                }
            }
            push( @resourceSpecs, $resourceSpec );
        }
    }
    else {
        my $resourceSpec = {
            apiVersion => 'apps/v1',
            kind       => 'Deployment',
            metadata   => {
                name => $deployment
            },
            spec => {
                template => {
                    spec => {
                        containers => $imageList
                    }
                }
            }
        };
        push( @resourceSpecs, $resourceSpec );
    }

    if ( $hasConfErr == 0 ) {
        my $result;

        foreach my $resourceSpec (@resourceSpecs) {
            my $kind       = $resourceSpec->{kind};
            my $apiVersion = $resourceSpec->{apiVersion};
            my $name       = $resourceSpec->{metadata}->{name};
            my $namespace  = $resourceSpec->{metadata}->{namespace};

            my $resExists = 1;
            my $rc        = $self->getResource( $kind, $apiVersion, $namespace, $name );
            if ( $rc->{kind} eq 'Status' ) {
                $resExists = 0;
                if ($autoCreate) {
                    print( 'WARN: ' . $rc->{message} . "\n" );
                }
                else {
                    $self->{hasError} = 1;
                    print( 'ERROR: ' . $rc->{message} . "\n" );
                    last;
                }
            }

            if ($resExists) {
                $result = $self->patchResource( $resourceSpec, $kind, $apiVersion, $namespace, $name );
            }
            else {
                $result = $self->createResource( $resourceSpec, $kind, $apiVersion, $namespace );
            }
            if ( not defined($result) ) {
                $self->{hasError} = 1;
                last;
            }
        }
        $self->refreshLiveData();

        return $result;
    }
}

sub rollbackDeployment {
    my ( $self, $resourceSpecs ) = @_;
    my $deployment = $self->{deployment};

    my $deployInfo     = $self->getDeployment();
    my $replicasetList = $self->getDeploymentReplicasets($deployInfo);
    my $replicasCount  = scalar(@$replicasetList);
    if ( @$replicasetList < 2 ) {
        print("WARN: There is no revision history for deployment $deployment, can not rollback.\n");
    }
    else {
        my $problemReplica         = $$replicasetList[0];
        my $problemName            = $problemReplica->{metadata}->{name};
        my $problemNamespace       = $problemReplica->{metadata}->{namespace};
        my $problemContainers      = $problemReplica->{spec}->{template}->{spec}->{containers};
        my $problemContainersCount = scalar(@$problemContainers);

        #找出最近的一个image不一样的版本的Pod
        my $rollbackTargetContainers;
        my $preReplica;
        for ( my $i = 1 ; $i < $replicasCount ; $i++ ) {
            $preReplica = $$replicasetList[$i];
            my $preContainers = $preReplica->{spec}->{template}->{spec}->{containers};

            my $preContainersCount = scalar(@$preContainers);
            if ( $problemContainersCount != $preContainersCount ) {
                last;
            }

            my $olderImageFound = 0;
            for ( my $j = 0 ; $j < $preContainersCount ; $j++ ) {
                my $aContainer = $$preContainers[$j];
                my $bContainer = $$problemContainers[$j];
                if ( $aContainer->{image} ne $bContainer->{image} ) {
                    $rollbackTargetContainers = $preContainers;
                    $olderImageFound          = 1;
                    last;
                }
            }
            if ( $olderImageFound == 1 ) {
                last;
            }
        }

        if ( defined($preReplica) ) {
            my $preName      = $preReplica->{metadata}->{name};
            my $preNamespace = $preReplica->{metadata}->{namespace};

            my $resourceSpecs = {
                spec => {
                    template => {
                        spec => {
                            containers => $rollbackTargetContainers
                        }
                    }
                }
            };

            my $rc = $self->patchDeployment($resourceSpecs);
            if ( defined($rc) ) {
                $self->refreshLiveData();
                print("INFO: Try to delete replica set $preName.\n");
                $self->deleteResource( 'ReplicaSet', 'apps/v1', $problemNamespace, $problemName );
                print("INFO: Replica set $preName deleted.\n");
            }
            return $rc;
        }
        else {
            print("ERROR: Can not find different pre image in replica set history.\n");
        }
    }
}

sub zeroReplicas {
    my ($self) = @_;

    my $deployInfo = $self->getDeployment();

    if ( $deployInfo->{spec}->{replicas} > 0 ) {
        my $annotations = $deployInfo->{metadata}->{annotations};

        my $metadata = $deployInfo->{metadata};
        delete( $metadata->{managedFields} );
        $metadata->{annotations} = {};
        $deployInfo->{status}    = {};

        $annotations->{'kubectl.kubernetes.io/last-applied-configuration'} = to_json($deployInfo);
        my $resourceSpecs = {

            metadata => {
                annotations => $annotations
            },
            spec => {
                replicas => 0
            }
        };

        my $rc = $self->patchDeployment($resourceSpecs);
        $self->refreshLiveData();
        return $rc;
    }
}

sub resumeReplicas {
    my ($self) = @_;

    my $deployInfo    = $self->getDeployment();
    my $preReplicas   = $self->{lastConf}->{spec}->{replicas};
    my $resourceSpecs = {
        spec => {
            replicas => $preReplicas
        }
    };
    my $rc = $self->patchDeployment($resourceSpecs);
    $self->refreshLiveData();
    return $rc;
}

sub restartDeployment {
    my ($self) = @_;

    my $deployInfo = $self->getDeployment();

    my $resourceSpecs = {
        spec => {
            metadata => {
                annotations => {
                    'kubectl.kubernetes.io/restartedAt' => strftime( "%Y-%m-%d %H:%M:%S", localtime() )
                }
            }
        }
    };

    my $rc = $self->patchDeployment($resourceSpecs);
    $self->refreshLiveData();
    return $rc;
}

sub getDeploymentDetail {
    my ($self) = @_;

    my $webCtl     = $self->{webCtl};
    my $baseUrl    = $self->{baseUrl};
    my $namespace  = $self->{namespace};
    my $deployment = $self->{deployment};

    my $data       = {};
    my $deployInfo = $self->getDeployment();
    $data->{deployment} = $deployInfo;

    my $replicasetList = $self->getDeploymentReplicasets($deployInfo);
    foreach my $replicasetInfo (@$replicasetList) {
        $replicasetInfo->{pods} = $self->getReplicasetPods( $replicasetInfo->{spec}->{selector}->{matchLabels} );
    }
    $data->{replicasets} = $replicasetList;

    return $data;
}

sub refreshLiveData {
    my ($self) = @_;

    my $action     = $self->{action};
    my $timeout    = $self->{timeout};
    my $deployment = $self->{deployment};
    my $startTime  = time();

    my $data = {};

    my $deployInfo = $self->getDeployment(0);
    $data->{deployment} = $deployInfo;

    while (1) {
        my $replicasetList  = $self->getDeploymentReplicasets($deployInfo);
        my $replicasetCount = scalar(@$replicasetList);

        for ( my $i = 0 ; $i < 2 and $i < $replicasetCount ; $i++ ) {
            my $replicasetInfo = $$replicasetList[$i];
            $replicasetInfo->{pods} = $self->getReplicasetPods( $replicasetInfo->{spec}->{selector}->{matchLabels} );
        }

        my $replicasetsCount = scalar(@$replicasetList);
        if ( $replicasetsCount > 0 ) {
            $data->{newreplicaset} = $$replicasetList[0];
        }
        if ( $replicasetsCount > 1 ) {
            $data->{oldreplicaset} = $$replicasetList[1];
        }
        else {
            $data->{oldreplicaset} = {};
        }

        $self->saveDeploymentStatus($data);
        if ( defined( $self->{dryRun} ) ) {
            last;
        }

        my $replicas;
        my $runningReplicas = 0;
        if (@$replicasetList) {

            #$replicas = $$replicasetList[0]->{status}->{replicas};
            $replicas = $self->{replicas};
            if ( not defined($replicas) or $replicas == 0 ) {
                $replicas = $$replicasetList[0]->{status}->{replicas};
            }

            my $pods = $$replicasetList[0]->{pods};
            foreach my $pod (@$pods) {
                if ( $pod->{status}->{phase} eq 'Running' ) {
                    my $isReady           = 1;
                    my $containerStatuses = $pod->{status}->{containerStatuses};
                    if ( defined($containerStatuses) ) {
                        foreach my $containerStatus (@$containerStatuses) {
                            if ( not $containerStatus->{ready} ) {
                                $isReady = 0;
                                last;
                            }
                        }
                    }
                    if ( $isReady == 1 ) {
                        $runningReplicas = $runningReplicas + 1;
                    }
                }
            }
            $self->tailPodsLog($pods);

            if ( $runningReplicas == $replicas ) {
                last;
            }
        }
        if ( time() - $startTime > $timeout ) {
            print("ERROR: Perform action $action for $deployment timeout.\n");
            $self->{hasError} = 1;
            last;
        }

        #等待5秒,因为使用了AnyEvent，不能使用sleep来延时，否则会破环AnyEvent的事件循环
        my $timerFired = AnyEvent->condvar;
        my $w          = AnyEvent->timer(
            after => 5,
            cb    => sub { $timerFired->send() },
        );
        print("INFO: Waiting pods ready...\n");
        $timerFired->recv();
        ###
    }

    #最后等待2秒,因为使用了AnyEvent，不能使用sleep来延时，否则会破环AnyEvent的事件循环
    my $timerFired = AnyEvent->condvar;
    my $w          = AnyEvent->timer(
        after => 5,
        cb    => sub { $timerFired->send() },
    );
    $timerFired->recv();
    print("INFO: Pods ready.\n");
    ###
}

sub getDeploymentReplicasets {
    my ( $self, $deployInfo ) = @_;
    my $webCtl     = $self->{webCtl};
    my $namespace  = $self->{namespace};
    my $deployment = $self->{deployment};

    my $labels        = $deployInfo->{spec}->{selector}->{matchLabels};
    my $labelSelector = join( ',', map { $_ . '=' . $labels->{$_} } keys(%$labels) );

    #my $url           = "$baseUrl/apis/apps/v1/namespaces/$namespace/replicasets?labelSelector=$labelSelector";
    my $url = $self->getApiUrl( 'Replicaset', 'apps/v1', $namespace, undef, { labelSelector => $labelSelector } );

    my $content = $webCtl->doRest( 'GET', $url );
    my $rc      = from_json($content);

    my @replicasets = ();

    if ( $rc->{kind} eq 'ReplicaSetList' ) {

        sub sortReplicaset($$) {
            my ( $a, $b ) = @_;
            my $aRevision = 0;
            my $bRevision = 0;
            if ( defined( $a->{metadata} ) and defined( $a->{metadata}->{annotations} ) ) {
                $aRevision = $a->{metadata}->{annotations}->{'deployment.kubernetes.io/revision'};
            }
            if ( defined( $b->{metadata} ) and defined( $b->{metadata}->{annotations} ) ) {
                $bRevision = $b->{metadata}->{annotations}->{'deployment.kubernetes.io/revision'};
            }
            return $bRevision <=> $aRevision;
        }
        my $replicasetList = $rc->{items};
        my @replicasets    = sort sortReplicaset (@$replicasetList);

        return \@replicasets;
    }
    else {
        print( 'WARN: ' . $rc->{message} . "\n" );
        return [];
    }
}

sub getReplicasetPods {
    my ( $self, $labels ) = @_;
    my $webCtl     = $self->{webCtl};
    my $namespace  = $self->{namespace};
    my $deployment = $self->{deployment};

    my $labelSelector = join( ',', map { $_ . '=' . $labels->{$_} } keys(%$labels) );

    #my $url          = "$baseUrl/api/v1/namespaces/$namespace/pods?labelSelector=$labelSelector";
    my $url = $self->getApiUrl( 'Pod', 'v1', $namespace, undef, { labelSelector => $labelSelector } );

    my $content = $webCtl->doRest( 'GET', $url );
    my $rc      = from_json($content);

    if ( $rc->{kind} eq 'PodList' ) {
        return $rc->{items};
    }
    else {
        print( 'WARN: ' . $rc->{message} . "\n" );
        return [];
    }
}

sub tailPodsLog {
    my ( $self, $pods ) = @_;
    my $webCtl         = $self->{webCtl};
    my $namespace      = $self->{namespace};
    my $restClient     = $webCtl->{restClient};
    my $headers        = $restClient->{_headers};
    my $tailLogPodsMap = $self->{tailLogPods};

    foreach my $pod (@$pods) {
        my $podName = $pod->{metadata}->{name};
        if ( not defined( $tailLogPodsMap->{$podName} ) ) {
            my $podStatus    = $pod->{status}->{phase};
            my $sinceSeconds = int( time() - $self->{updateTime} ) || 1;

            #my $url          = "$baseUrl/api/v1/namespaces/$namespace/pods/$podName/log?sinceSeconds=$sinceSeconds&follow=true";
            my $url       = my $url = $self->getApiUrl( 'Pod', 'v1', $namespace, "$podName/log", { sinceSeconds => $sinceSeconds, follow => 'true' }, $podName );
            my $httpGuard = AnyEvent::HTTP::http_request(
                GET       => $url,
                headers   => $headers,
                on_header => sub {
                    my ($hdr) = @_;
                    if ( $hdr->{Status} == 404 ) {
                        if ( $self->{action} ne 'stop' ) {
                            print("ERROR: Pod $namespace/pods/$podName not exists.\n");
                        }
                        else {
                            print("FINE: Pod $namespace/pods/$podName stopped.\n");
                        }
                    }
                    1;
                },
                on_body => sub {
                    my ( $data, $hdr ) = @_;
                    my $statusCode = $hdr->{Status};
                    if ( $statusCode =~ /^4/ ) {
                        $tailLogPodsMap->{$podName} = undef;
                        my $rc     = from_json($data);
                        my $errMsg = $rc->{reason} . ' ' . $rc->{message};
                        if ( $rc->{status} eq 'Failure' ) {
                            if ( $errMsg !~ /ContainerCreating/ ) {

                                #$self->{hasError} = 1;
                                print("WARN: $podName($statusCode) - $errMsg \n");
                            }
                        }
                        else {
                            print( $errMsg . "\n" );
                        }
                    }
                    else {
                        print( $podName . "-> " . $data );
                    }
                    1;
                },
                sub {
                }
            );
            $tailLogPodsMap->{$podName} = $httpGuard;
        }
    }
}

1;

sub main {
    my ( $host, $apiPort, $token );
    my ( $dplyYamlFile, $imagesTxt, $namespace, $deployment, $action, $timeout );
    my $isDryrun          = 0;
    my $autoCreate        = 0;
    my $healthCheckNeeded = 0;
    GetOptions(
        'host=s'              => \$host,
        'apiport=s'           => \$apiPort,
        'token=s'             => \$token,
        'dplyyamlfile=s'      => \$dplyYamlFile,
        'namespace=s'         => \$namespace,
        'deployment=s'        => \$deployment,
        'action=s'            => \$action,
        'dryrun=i'            => \$isDryrun,
        'autocreate:s'        => \$autoCreate,
        'healthcheckneeded:s' => \$healthCheckNeeded,
        'timeout=i'           => \$timeout,
        'imagelist=s'         => \$imagesTxt
    );

    $| = 1;

    my $optionError = 0;

    if ( ( not defined($host) or $host eq '' ) and ( not defined($apiPort) or $apiPort eq '' ) ) {
        print("ERROR: Must defind ip and apiPort for k8s api server.\n");
        $optionError = 1;
    }

    if ( not defined($token) or $token eq '' ) {
        print("ERROR: Must defind Authenticate token for k8s api server.\n");
        $optionError = 1;
    }

    if ( not defined($namespace) or $namespace eq '' ) {
        print("ERROR: Must defind namespace\n");
        $optionError = 1;
    }

    if ( not defined($deployment) or $deployment eq '' ) {
        print("ERROR: Must defind deployment name\n");
        $optionError = 1;
    }

    if ( not defined($timeout) ) {
        $timeout = 120;
    }
    my $org_timeout = $timeout;

    my @imageList = ();
    $imagesTxt =~ s/\\n/\n/gs;
    foreach my $image ( split( /\n|,/, $imagesTxt ) ) {
        my $name = lc($image);
        $image =~ s/^\s+|\s+$//g;
        $name  =~ s/^.*\/|:.*$//g;
        push( @imageList, { name => $name, image => $image } );
    }

    my $dryRun;
    if ( $isDryrun == 1 ) {
        $dryRun = 'All';
    }

    my $baseUrl = "https://$host:$apiPort";

    my $k8sDeploy = K8sDeploy->new(
        baseUrl           => $baseUrl,
        token             => $token,
        namespace         => $namespace,
        deployment        => $deployment,
        action            => $action,
        dryRun            => $dryRun,
        timeout           => $timeout,
        healthCheckNeeded => $healthCheckNeeded
    );

    eval {
        my $rc;

        if ( $action eq 'deploy' ) {
            $rc = $k8sDeploy->deployVersion( $dplyYamlFile, \@imageList, $autoCreate );
        }
        elsif ( $action eq 'rollback' ) {
            $rc = $k8sDeploy->rollbackDeployment();
        }
        elsif ( $action eq 'stop' ) {
            $rc = $k8sDeploy->zeroReplicas();
        }
        elsif ( $action eq 'start' ) {
            $rc = $k8sDeploy->resumeReplicas();
        }
        elsif ( $action eq 'restart' ) {
            $rc = $k8sDeploy->restartDeployment();
        }
        else {
            $k8sDeploy->{hasError} = 1;
            print("ERROR: Deployment $k8sDeploy->{deployment} $action not supported.\n");
        }

        if ( $k8sDeploy->{hasError} == 1 or not defined($rc) ) {
            print("ERROR: Deployment $k8sDeploy->{deployment} $action failed.\n");
        }
        else {
            print("FINE: Deployment $k8sDeploy->{deployment} $action succeed.\n");
        }
    };
    if ($@) {
        $k8sDeploy->{hasError} = 1;
        my $msg = $@;
        $msg =~ s/ at .*?$//;
        print($msg);
    }

    return $k8sDeploy->{hasError};
}

exit main();
